Skip to content

fix(web): expose GitHub install handlers, simplify Alpine loader, explicit Flask threading#305

Merged
ChuckBuilds merged 1 commit into
mainfrom
fix/safari-plugins-button-and-iife-scoping
Apr 8, 2026
Merged

fix(web): expose GitHub install handlers, simplify Alpine loader, explicit Flask threading#305
ChuckBuilds merged 1 commit into
mainfrom
fix/safari-plugins-button-and-iife-scoping

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Summary

A user reported that the Plugin Manager button (and, they said, "every button") was unresponsive in Safari after a fresh install. Their screenshots actually showed Alpine.js running fine end-to-end — the real problems are a narrow handler-exposure bug, some latent brittleness in the Alpine loader, and an implicit reliance on Flask's default threading for SSE. Fixing all three in one pass.

  • plugins_manager.jsattachInstallButtonHandler never exposed on window (the bug the reporter's console actually shows). The function is declared at line 5756 inside the main IIFE that closes at line 7164. The typeof guards meant to copy it to window sat outside the IIFE at lines 7394-7401, so typeof attachInstallButtonHandler always evaluated to 'undefined' and the assignment was silently skipped. The "Install from GitHub URL" button therefore had no click handler and [FALLBACK] attachInstallButtonHandler not available on window fired on every page load. Fixed by assigning window.attachInstallButtonHandler and window.setupGitHubInstallHandlers inside the IIFE, and removing the dead outside-the-IIFE guards.
  • base.html — simplified Alpine.js loader. Replaced the ~50-line dynamic-script + deferLoadingAlpine + isAPMode branching block with a single <script defer src="{{ url_for('static', filename='v3/js/alpinejs.min.js') }}"></script> plus a small window.load rescue that only pulls the unpkg CDN copy if window.Alpine is still undefined. script.defer = true on a dynamically-inserted <script> was a no-op (dynamic scripts are always async), the deferLoadingAlpine wrapper was cargo-culted, and the AP-mode branch reached out to unpkg unnecessarily on LAN installs even though alpinejs.min.js already ships in the repo at web_interface/static/v3/js/.
  • start.py / app.py — explicit threaded=True on app.run(...). Not a behavior change (Flask has defaulted to threaded=True since 1.0), but makes it self-documenting. The two long-lived /api/v3/stream/* SSE endpoints would starve every other request under a single-threaded dev server, and this guards against future regressions if someone disables threading.

Not changed (intentional)

  • The Alpine loader rewrite does not alter the working behavior observed in the reporter's screenshots — their Alpine was already initializing fine. This is cleanup, not "the fix".
  • The [RENDER] installed-plugins-grid not yet available, deferring render until plugin tab loads warning at plugins_manager.js:1378 is expected behavior (the grid lives inside an HTMX-lazy-loaded tab) and is left in place. Can be downgraded to pluginLog in a separate noise-cleanup PR if desired.
  • systemd/ledmatrix-web.service sets Environment=USE_THREADING=1, but that env var is never read anywhere in the repo. Left as-is since it's dead config and touching the systemd template affects install scripts.

Does this fix the reporter's symptom?

Partially. Fix #1 will silence the [FALLBACK] warning they're seeing and restore the GitHub "Install from URL" button. But their claim was "Plugin Manager button or any button won't work", and since the screenshots prove Alpine's click layer is healthy on their machine, the "any button" part is almost certainly something environment-specific (Safari version, a stuck overlay, an uncaught exception scrolled out of the console) that can't be diagnosed from the screenshots alone. A follow-up diagnostic message has been sent to collect more data from them.

Test plan

  • git pull, hard-reload (Cmd+Opt+R in Safari), confirm the Plugin Manager tab still opens and renders.
  • In the browser console, confirm typeof window.attachInstallButtonHandler === 'function' and typeof window.setupGitHubInstallHandlers === 'function'.
  • Confirm the [FALLBACK] attachInstallButtonHandler not available on window warning no longer appears in the console.
  • Paste a GitHub plugin URL into the "Install from URL" form on the Plugin Store tab and confirm the submit button actually fires its handler.
  • In the Network tab, confirm alpinejs.min.js loads from /static/v3/js/ (200) and no request goes to unpkg.com.
  • Confirm window.Alpine is defined and tab switching still works across Overview / Settings / Logs / Plugin Manager.
  • Regression-check in Chrome and Firefox.
  • If accessible, regression-check in AP mode (192.168.4.1) — the local-first loader should work identically there.
  • Watch the two /api/v3/stream/* SSE connections in the Network tab for several minutes under load — confirm they stay open (Connected indicator green) and don't interrupt other requests.

🤖 Generated with Claude Code

…licit Flask threading

A user reported that buttons in the v3 web UI were unresponsive in Safari
after a fresh install. The screenshots showed Alpine.js actually running
fine end-to-end — the real issues are a narrow handler-exposure bug and
some latent brittleness worth cleaning up at the same time.

plugins_manager.js: attachInstallButtonHandler and setupGitHubInstallHandlers
were declared inside the main IIFE, but the typeof guards that tried to
expose them on window ran *outside* the IIFE, so typeof always evaluated
to 'undefined' and the assignments were silently skipped. The GitHub
"Install from URL" button therefore had no click handler and the console
printed [FALLBACK] attachInstallButtonHandler not available on window on
every load. Fixed by assigning window.attachInstallButtonHandler and
window.setupGitHubInstallHandlers *inside* the IIFE just before it closes,
and removing the dead outside-the-IIFE guards.

base.html: the Alpine.js loader was a 50-line dynamic-script + deferLoadingAlpine
+ isAPMode branching block. script.defer = true on a dynamically-inserted
<script> is a no-op (dynamic scripts are always async), the
deferLoadingAlpine wrapper was cargo-culted, and the AP-mode branching
reached out to unpkg unnecessarily on LAN installs even though
alpinejs.min.js already ships in web_interface/static/v3/js/. Replaced
with a single <script defer src="..."> tag pointing at the local file plus
a small window-load rescue that only pulls the CDN copy if window.Alpine
is still undefined.

start.py / app.py: app.run() has defaulted to threaded=True since Flask
1.0 so this is not a behavior change, but the two long-lived
/api/v3/stream/* SSE endpoints would starve every other request under a
single-threaded server. Setting threaded=True explicitly makes the
intent self-documenting and guards against future regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ChuckBuilds ChuckBuilds merged commit 9412915 into main Apr 8, 2026
@ChuckBuilds ChuckBuilds deleted the fix/safari-plugins-button-and-iife-scoping branch April 8, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant